import React, {Component} from 'react'
import {Popper as ReactPopper} from 'react-popper'
import * as PopperJS from '@popperjs/core'
import {getTarget, isInDOMSubtrees, keyCodes} from '../utils/utils'

interface Delays {
  show: number
  hide: number
}

interface PopoverWrapperProps {
  target: string
  placement: PopperJS.Placement
  trigger: 'click' | 'hover' | 'focus' | 'keydown' | 'legacy'
  disabled?: boolean
  autoHide?: boolean
  className?: string
  isOpen: boolean
  toggle: (e: any) => void
  role: string
}

interface PopoverWrapperState {
  isShow: boolean
  targets: any[]
  currentTargetElement: any
  hideTimeout: any
  showTimeout: any
  popover: any
}

const DEFAULT_DELAYS: Delays = {
  show: 0,
  hide: 50,
}

class PopoverWrapper extends Component<PopoverWrapperProps, PopoverWrapperState> {
  _isMounted = false

  constructor(props: any) {
    super(props)
    this.state = {
      isShow: false,
      targets: [],
      currentTargetElement: null,
      hideTimeout: null,
      showTimeout: null,
      popover: null,
    }
  }

  componentDidMount(): void {
    this._isMounted = true
    this.updateTarget()
  }

  componentWillUnmount() {
    this.removeTargetEvents()
    this.setState({targets: null})
    this.clearShowTimeout()
    this.clearHideTimeout()
    this._isMounted = false
  }

  getRef = (ref: any) => {
    const popover: any = ref
    this.setState({popover})
  }

  updateTarget = () => {
    const {target} = this.props
    let {targets, currentTargetElement} = this.state
    const newTarget: any = getTarget(target, true)
    if (newTarget !== targets) {
      this.removeTargetEvents()
      targets = newTarget ? Array.from(newTarget) : []
      currentTargetElement = currentTargetElement || targets[0]
      this.setState(
        {
          targets,
          currentTargetElement,
        },
        () => {
          this.addTargetEvents()
        },
      )
    }
  }

  addTargetEvents = () => {
    const {trigger} = this.props
    const {targets} = this.state
    if (trigger) {
      let triggers = trigger.split(' ')
      if (triggers.indexOf('manual') === -1) {
        if (triggers.indexOf('click') > -1 || triggers.indexOf('legacy') > -1) {
          document.addEventListener('click', this.handleDocumentClick, true)
        }
        if (targets && targets.length) {
          if (triggers.indexOf('hover') > -1) {
            this.addEventOnTargets('mouseover', this.showWithDelay, true)
            this.addEventOnTargets('mouseout', this.hideWithDelay, true)
          }

          if (triggers.indexOf('focus') > -1) {
            this.addEventOnTargets('focusin', this.show, true)
            this.addEventOnTargets('focusout', this.hide, true)
          }
          this.addEventOnTargets('keydown', this.onKeyDown, true)
        }
      }
    }
  }

  getMenuItems() {
    const {popover} = this.state
    return [].slice.call(popover.querySelectorAll('[role="menuitem"]'))
  }

  onKeyDown = (e: any) => {
    if (e.key === 'Escape') {
      this.hide(e)
    }
    const {isOpen} = this.props
    if (isOpen) {
      if (
        [keyCodes.down, keyCodes.up].indexOf(e.which) > -1 ||
        ([keyCodes.n, keyCodes.p].indexOf(e.which) > -1 && e.ctrlKey)
      ) {
        const $menuitems = this.getMenuItems()
        let index = $menuitems.indexOf(e.target)
        if (keyCodes.up === e.which || (keyCodes.p === e.which && e.ctrlKey)) {
          index = index !== 0 ? index - 1 : $menuitems.length - 1
        } else if (keyCodes.down === e.which || (keyCodes.n === e.which && e.ctrlKey)) {
          index = index === $menuitems.length - 1 ? 0 : index + 1
        }
        $menuitems[index].focus()
      }
    }
  }

  addEventOnTargets = (type: string, handler: (e: any) => void, isBubble: boolean) => {
    const {targets} = this.state
    targets.forEach((target) => {
      target.addEventListener(type, handler, isBubble)
    })
  }

  removeEventOnTargets = (type: string, handler: (e: any) => void, isBubble: boolean) => {
    const {targets} = this.state
    targets.forEach((target) => {
      target.removeEventListener(type, handler, isBubble)
    })
  }

  handleDocumentClick = (e: any) => {
    const {trigger, isOpen} = this.props
    const {targets, hideTimeout, popover} = this.state
    const triggers = trigger.split(' ')
    const popoverArea = popover && popover.querySelector('[role="menu"]')

    if (triggers.indexOf('legacy') > -1 && (isOpen || isInDOMSubtrees(e.target, targets))) {
      if (hideTimeout) {
        this.clearHideTimeout()
      }
      if (isOpen) {
        this.hideWithDelay(e)
      } else if (!isOpen) {
        this.showWithDelay(e)
      }
    } else if (triggers.indexOf('click') > -1 && isInDOMSubtrees(e.target, popoverArea)) {
      if (hideTimeout) {
        this.clearHideTimeout()
      }

      if (!isOpen) {
        this.showWithDelay(e)
      } else {
        this.hideWithDelay(e)
      }
    }
  }

  hideWithDelay = (e: any) => {
    const {showTimeout} = this.state
    if (showTimeout) {
      this.clearShowTimeout()
    }
    let hideTimeout = setTimeout(() => {
      this.hide(e)
      this.getDelay('hide')
    })
    this.setState({hideTimeout})
  }

  showWithDelay = (e: any) => {
    const {hideTimeout} = this.state
    if (hideTimeout) {
      this.clearHideTimeout()
    }
    const showTimeout = setTimeout(() => {
      this.show(e)
      this.getDelay('show')
    })
    this.setState({showTimeout})
  }

  hide = (e: any) => {
    const {isOpen} = this.props
    if (isOpen) {
      this.clearHideTimeout()
      let currentTargetElement = null
      this.toggle(e)
      this.setState({currentTargetElement})
    }
  }

  getDelay = (key: 'show' | 'hide') => {
    return DEFAULT_DELAYS[key]
  }

  show = (e: any) => {
    const {isOpen, target} = this.props
    if (!isOpen) {
      this.clearShowTimeout()
      let currentTargetElement = document.getElementById(target)
      this.setState({currentTargetElement})
      this.toggle(e)
    }
  }

  toggle = (e?: any) => {
    const {disabled, toggle} = this.props
    if (disabled || !this._isMounted) {
      return e && e.preventDefault()
    }
    toggle(e)
  }

  clearHideTimeout = () => {
    const {hideTimeout} = this.state
    clearTimeout(hideTimeout)
    this.setState({hideTimeout: undefined})
  }

  clearShowTimeout = () => {
    const {showTimeout} = this.state
    clearTimeout(showTimeout)
    this.setState({showTimeout: undefined})
  }

  removeTargetEvents = () => {
    const {targets} = this.state
    if (targets) {
      this.removeEventOnTargets('mouseover', this.showWithDelay, true)
      this.removeEventOnTargets('mouseout', this.hideWithDelay, true)
      this.removeEventOnTargets('keydown', this.onKeyDown, true)
      this.removeEventOnTargets('focusin', this.show, true)
      this.removeEventOnTargets('focusout', this.hide, true)
    }
    document.removeEventListener('click', this.handleDocumentClick, true)
  }

  onMouseOverPopoverContent = () => {
    const {autoHide, isOpen} = this.props
    const {hideTimeout} = this.state
    if (this.props.trigger.indexOf('hover') > -1 && !autoHide) {
      if (hideTimeout) {
        this.clearHideTimeout()
      }
      if (isOpen && !isOpen) {
        this.toggle()
      }
    }
  }

  onMouseLeavePopoverContent = (e: any) => {
    const {showTimeout} = this.state
    if (this.props.trigger.indexOf('hover') > -1 && !this.props.autoHide) {
      if (showTimeout) {
        this.clearShowTimeout()
      }
      e.persist()
      let hideTimeout = setTimeout(() => {
        this.hide(e)
        this.getDelay('hide')
      })
      this.setState({hideTimeout})
    }
  }

  render(): React.ReactNode {
    const {children, placement, className, isOpen, role} = this.props
    const {currentTargetElement} = this.state
    if (!isOpen) {
      return null
    }
    return (
      <ReactPopper
        referenceElement={currentTargetElement}
        placement={placement}
        // positionFixed={true}
        modifiers={
          placement === 'right-start'
            ? [
                {name: 'offset', enabled: true, options: {offset: [20, -15]}},
                {name: 'flip', enabled: false},
              ]
            : []
        }>
        {({ref, style}) => (
          <div ref={this.getRef}>
            <div
              ref={ref}
              style={style}
              className={className}
              role={role}
              onMouseOver={this.onMouseOverPopoverContent}
              onMouseLeave={this.onMouseLeavePopoverContent}
              onKeyDown={this.onKeyDown}
              children={children}
            />
          </div>
        )}
      </ReactPopper>
    )
  }
}

export default PopoverWrapper
